package com.lhkj.ct.framework.shiro.config;

import com.lhkj.ct.base.constants.Constants;
import com.lhkj.ct.base.constants.SystemConstants;
import com.lhkj.ct.base.model.LoginUser;
import com.lhkj.ct.framework.shiro.properties.ShiroProperties;
import com.lhkj.ct.base.utils.EnvironmentUtils;
import com.lhkj.ct.base.utils.LambdaUtils;
import com.lhkj.ct.framework.shiro.authc.pam.CustomModularRealmAuthenticator;
import com.lhkj.ct.framework.shiro.credential.RetryLimitHashedCredentialsMatcher;
import com.lhkj.ct.framework.shiro.filter.MyFormAuthenticationFilter;
import com.lhkj.ct.framework.shiro.realm.AdminAuthorizingRealm;
import com.lhkj.ct.framework.shiro.realm.PlatformAuthorizingRealm;
import com.lhkj.ct.framework.shiro.realm.UserIdentityRealm;
import com.lhkj.ct.framework.shiro.session.ShiroSessionManager;
import org.apache.shiro.authc.AbstractAuthenticator;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler;
import org.apache.shiro.session.mgt.ValidatingSessionManager;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.InvalidRequestFilter;
import org.apache.shiro.web.filter.mgt.DefaultFilter;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.core.env.Environment;
import org.springframework.transaction.config.TransactionManagementConfigUtils;

import javax.annotation.Resource;
import javax.servlet.Filter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * <p>
 *     shiro安全框架相关配置
 * </p>
 */
@Configuration
public class ShiroSecurityConfig {

    @Resource
    private RedisProperties redisProperties;

    @Resource
    private ShiroProperties shiroProperties;

    @Resource
    private Environment environment;



    /**
     * ShiroFilterFactoryBean 处理拦截资源文件问题。
     * 注意：初始化ShiroFilterFactoryBean的时候需要注入：SecurityManager
     * Web应用中,Shiro可控制的Web请求必须经过Shiro主过滤器的拦截
     *
     * @return org.apache.shiro.spring.web.ShiroFilterFactoryBean
     */
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilterFactoryBean() {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        Filter filter = factoryBean.getFilters().get(DefaultFilter.invalidRequest.name());
        if (filter instanceof InvalidRequestFilter) {
            ((InvalidRequestFilter) filter).setBlockNonAscii(false);
        }
        // 必须设置 SecurityManager,Shiro的核心安全接口
        factoryBean.setSecurityManager(securityManager());
        // 由于前后端分离，这里不在处理登录页面
        // 设置默认登录页面，如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
//        factoryBean.setLoginUrl(shiroProperties.getLoginUrl());

        /**
         * 配置自定义的验证过滤器，根据用户角色执行不同的登录，失效等跳转
         */
        Map<String, Filter> filtersMap = new HashMap<>();
        MyFormAuthenticationFilter authFilter = new MyFormAuthenticationFilter();
        filtersMap.put("authc", authFilter);
        factoryBean.setFilters(filtersMap);

        /**
         * 过滤器配置, 配置访问权限 使用LinkedHashMap，因为它必须保证有序
         *
         * tips 过滤链定义，从上向下顺序执行，一般将 /**放在最为下边 一定要注意顺序,否则就不好使了
         */
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 排除静态资源
        if (!shiroProperties.getAnon().isEmpty()) {
            for (String anon : shiroProperties.getAnon()) {
                filterChainDefinitionMap.put(anon, "anon");
            }
        }
        // 放行文件路径
        filterChainDefinitionMap.put(Constants.RESOURCE_PREFIX + "/**", "anon");
        // 所有url拦截
        if (!shiroProperties.getAuth().isEmpty()) {
            for (String anon : shiroProperties.getAuth()) {
                filterChainDefinitionMap.put(anon, "authc");
            }
        }
        factoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return factoryBean;
    }

    /**
     * 配置shiro安全事务管理器
     *
     * 如在realm内注入了服务层，服务层又使用了事务注解会报错
     * 方案一：使用@DependsOn注解
     *
     * SpringBoot默认的基于JDK的动态代理模式（transaction advisor）
     * TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME
     * 对应的全限定名称就是：org.springframework.transaction.config.internalTransactionAdvisor
     *
     * Spring的cglib切面动态代理模式（transaction aspect）
     * TransactionManagementConfigUtils.TRANSACTION_ASPECT_BEAN_NAME
     * 对应的全限定名称就是：org.springframework.transaction.config.internalTransactionAspect
     *
     * 方案二：定义InitListener来监听ApplicationContext
     * 详情参考 {@see <a>https://blog.csdn.net/tearon/article/details/105552158</a>}
     */
    @Bean(name = "securityManager")
    @DependsOn(TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME)
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setCacheManager(cacheManager());
        securityManager.setSessionManager(getSessionManage());
//        securityManager.setRealms(Arrays.asList(webAuthorizingRealm(), accountAuthorizingRealm(), userIdentityRealm()));
        securityManager.setAuthenticator(abstractAuthenticator());
//        /*
//         * 关闭shiro自带的session，详情见文档
//         * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
//         */
//        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
//        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
//        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
//        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
//        securityManager.setSubjectDAO(subjectDAO);
        return securityManager;
    }

    @Bean(name = "abstractAuthenticator")
    public AbstractAuthenticator abstractAuthenticator(){
        ModularRealmAuthenticator authenticator = new CustomModularRealmAuthenticator();
        authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
        authenticator.setRealms(Arrays.asList(webAuthorizingRealm(), accountAuthorizingRealm(), userIdentityRealm()));
        return authenticator;
    }

    /**
     * web授权（账号密码方式）
     */
    @Bean
    public AuthorizingRealm webAuthorizingRealm() {
        AuthorizingRealm realm = new AdminAuthorizingRealm(cacheManager(),  getHashCredentialsMatcher());
        realm.setAuthenticationCache(cacheManager().getCache(realm.getName()));
        return realm;
    }

    /**
     * web授权（账号密码方式）
     */
    @Bean
    public AuthorizingRealm accountAuthorizingRealm() {
        AuthorizingRealm realm = new PlatformAuthorizingRealm(cacheManager(), getHashCredentialsMatcher());
        realm.setAuthenticationCache(cacheManager().getCache(realm.getName()));
        return realm;
    }

    /**
     * 第三方授权（账号密码方式）
     */
    @Bean
    public AuthorizingRealm userIdentityRealm() {
        AuthorizingRealm realm = new UserIdentityRealm(cacheManager());
        realm.setAuthenticationCache(cacheManager().getCache(realm.getName()));
        return realm;
    }

    /**
     * 使用的加密方式MD5哈希，并设置迭代加密次数
     */
    @Bean(name = "hashCredentialsMatcher")
    public HashedCredentialsMatcher getHashCredentialsMatcher() {
        HashedCredentialsMatcher credentialsMatcher = new RetryLimitHashedCredentialsMatcher();
        // 设置加密方式MD5
        credentialsMatcher.setHashAlgorithmName(Md5Hash.ALGORITHM_NAME);
        // 设置加密次数
        credentialsMatcher.setHashIterations(SystemConstants.HASH_ITERATIONS);
        // storedCredentialsHexEncoded 默认是true此时用的是密码加密用的是Hex编码；false时用Base64编码
        credentialsMatcher.setStoredCredentialsHexEncoded(true);//转化为16进制(与入库时保持一致)
        return credentialsMatcher;
    }

    /**
     * shiro redis连接配置
     */
    @Bean
    public RedisManager redisManager() {
        // 设置redis配置：端口，库、密码
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(redisProperties.getHost() + ":" + redisProperties.getPort());
        redisManager.setDatabase(redisProperties.getDatabase());
        redisManager.setPassword(redisProperties.getPassword());
        return redisManager;
    }

    /**
     * shiro redis缓存管理
     * CacheManager即缓存管理，将用户权限数据存储在缓存，这样可以提高性能。
     */
    @Bean(name = "redisCacheManager")
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        // 设置redis的key前缀(这里加上项目名)，默认为shiro:cache:
        redisCacheManager.setKeyPrefix(EnvironmentUtils.resolveString(environment, "server.servlet.context-path", "").replace("/", "") + "shiro:cache:");
        // redis中针对不同用户缓存
        redisCacheManager.setPrincipalIdFieldName(LambdaUtils.getName(LoginUser::getSessionKey));
        // redis存储的session有效时间，默认为1800，-1即与session过期时间相同； -2为永不过期
//        redisCacheManager.setExpire(24 * 60 * 60);
        return redisCacheManager;
    }

    /**
     * session 校验规则设置
     */
    @Bean(name = "sessionValidationScheduler")
    public ExecutorServiceSessionValidationScheduler configSessionValidationScheduler(ValidatingSessionManager sessionManager) {
        ExecutorServiceSessionValidationScheduler sessionValidationScheduler = new ExecutorServiceSessionValidationScheduler(sessionManager);
        // 设置session的失效扫描间隔此处为20分钟，单位(毫秒)
        sessionValidationScheduler.setInterval(20 * 60 * 1000);
        return sessionValidationScheduler;
    }

    @Bean(name = "sessionIdCookie")
    public SimpleCookie sessionIdCookie() {
        // 初始化、默认cookie的name
        SimpleCookie cookie = new SimpleCookie("auth-token");
        // 防止通过js脚本窃取cookie内容，进行xss等攻击，默认是true
        cookie.setHttpOnly(true);
        // true表示"指示浏览器仅通过 HTTPS 连接传回 cookie。这可以确保 cookie ID 是安全的，且仅用于使用 HTTPS 的网站。
        // 如果启用此功能，则 HTTP 上的会话 Cookie 将不再起作用。
        cookie.setSecure(false);
        return cookie;
    }

//    @Bean(name = "rememberMeCookie")
//    public SimpleCookie rememberMeCookie() {
//        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
//        // 有效期，单位是秒，这里是一天
//        simpleCookie.setMaxAge(7 * 24 * 60 * 60);
//        return simpleCookie;
//    }

    /**
     * SessionDAO即会话dao，是对session会话操作的一套接口，比如要将session存储到数据库，可以通过jdbc将会话存储到数据库
     */
    @Bean(name = "sessionDAO")
    public SessionDAO sessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        // session在redis中的保存时间此处为两小时+一分钟,最好大于session会话超时时间(单位：秒)
        redisSessionDAO.setExpire(24 * 60 * 60 + 60);
        return redisSessionDAO;
    }

//    /**
//     * TODO  cookie管理对象;记住我功能,rememberMe管理器
//     */
//    @Bean(name = "cookieRememberMeManager")
//    public CookieRememberMeManager cookieRememberMeManager() {
//        CookieRememberMeManager meManager = new CookieRememberMeManager();
//        // 使用aes加密方式
//        AesCipherService cipherService = new AesCipherService();
//        meManager.setCipherKey(cipherService.generateNewKey().getEncoded());
//        meManager.setCookie(rememberMeCookie());
//        return meManager;
//    }

    @Bean(name = "sessionManager")
    public ShiroSessionManager getSessionManage() {
        ShiroSessionManager sessionManager = new ShiroSessionManager();
        // 会话超时时间此处设置为两小时，单位：毫秒
        sessionManager.setGlobalSessionTimeout(24 * 3600 * 1000);
        sessionManager.getSessionIdCookie().setMaxAge(24 * 3600 * 1000);
        sessionManager.setSessionValidationScheduler(configSessionValidationScheduler(sessionManager));
        // 是否开启定时清理失效会话
        sessionManager.setSessionValidationSchedulerEnabled(true);
        // 默认是true
        sessionManager.setDeleteInvalidSessions(true);
        // 是否允许将sessionId 放到 cookie 中
        sessionManager.setSessionIdCookieEnabled(true);
        // 指定sessionId
        sessionManager.setSessionIdCookie(sessionIdCookie());
        // 默认使用MemerySessionDao，设置为sessionDAO
        sessionManager.setSessionDAO(sessionDAO());
        // 是否允许将 sessionId 放到 Url 地址拦中  第一次重定向时，会在url后携带jsessionid，这会导致400错误（无法找到该网页）。
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        return sessionManager;
    }

    @Bean
    public MethodInvokingFactoryBean getMethodInvokingFactoryBean() {
        MethodInvokingFactoryBean factoryBean = new MethodInvokingFactoryBean();
        factoryBean.setStaticMethod("org.apache.shiro.SecurityUtils.setSecurityManager");
        factoryBean.setArguments(securityManager());
        return factoryBean;
    }

    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator getAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 强制使用cglib，防止重复代理和可能引起代理出错的问题
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    /**
     * 开启shiro注解 tips 方法前需要加static或者单独创建配置类进行注入
     */
    @Bean
    public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 开启shiro注解（例如@RequiresRoles和//@RequiresPermissions）
     * 详见 {@see <a src="https://blog.csdn.net/qq_26383975/article/details/109243060"></a>}
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager());
        return advisor;
    }

//    //2. 配置ShiroDialect，用于Shiro和thymeleaf标签配合使用
//    //若不配置，可能导致前端使用shiro标签后无效果
//    @Bean(name = "shiroDialect")
//    public ShiroDialect shiroDialect() {
//        return new ShiroDialect();
//    }
}
